package com.yoooya.base.third.service;

import com.alibaba.fastjson.JSON;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.yoooya.base.dao.third.AccessTokenMapper;
import com.yoooya.base.model.po.third.AccessTokenPo;
import com.yoooya.base.third.common.error.WxError;
import com.yoooya.base.third.common.error.WxErrorException;
import com.yoooya.base.third.model.vo.WxAccessToken;
import com.yoooya.base.third.model.vo.WxJsapiSignature;
import com.yoooya.base.third.model.vo.WxMediaUploadResult;
import com.yoooya.base.third.model.vo.WxMenu;
import com.yoooya.base.third.model.vo.cp.*;
import com.yoooya.base.third.utils.crypto.SHA1;
import com.yoooya.base.third.utils.fs.FileUtils;
import com.yoooya.base.third.utils.http.*;
import com.yoooya.base.third.utils.json.GsonHelper;
import com.yoooya.base.third.utils.json.cp.WxCpGsonBuilder;
import com.yoooya.base.utils.HttpClientUtils;
import com.yoooya.base.utils.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * 微信API的Service
 * 
 * @创建人 Sun Yijing
 * @创建时间 2018-07-08
 */
@Service
@Transactional
public class WxCpService {

	@Autowired
	private AccessTokenMapper accessTokenMapper;

	@Value("${wx.cp.corpId}")
	private String corpId;
	@Value("${wx.cp.agentId}")
	private String agentId;

	protected final Logger log = LoggerFactory.getLogger(WxCpService.class);

	/**
	 * 全局的是否正在刷新access token的锁
	 */
	protected final Object globalAccessTokenRefreshLock = new Object();

	/**
	 * 全局的是否正在刷新jsapi_ticket的锁
	 */
	protected final Object globalJsapiTicketRefreshLock = new Object();

	private int retrySleepMillis = 1000;

	private int maxRetryTimes = 5;

	/**
	 * 临时文件目录
	 */
	private File tmpDirFile;

	/**
	 * <pre>
	 * 验证推送过来的消息的正确性
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=验证消息真实性
	 * </pre>
	 *
	 * @param msgSignature
	 * @param timestamp
	 * @param nonce
	 * @param data
	 *            微信传输过来的数据，有可能是echoStr，有可能是xml消息
	 * @return
	 */
	public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data, String token) {
		try {
			return SHA1.gen(token, timestamp, nonce, data).equals(msgSignature);
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * <pre>
	 *   用在二次验证的时候
	 *   企业在员工验证成功后，调用本方法告诉企业号平台该员工关注成功。
	 * </pre>
	 *
	 * @param userId
	 */
	public void userAuthenticated(String userId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?userid=" + userId;
		String code = "";
		get(url, null, code);
	}

	/**
	 * 获得jsapi_ticket,不强制刷新jsapi_ticket
	 * 
	 * @see #getJsapiTicket(boolean)
	 * @return
	 * @throws WxErrorException
	 */
	public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
		WxCpAccessToken token = queryAccessToken();
		if (forceRefresh) {
			token.expireJsapiTicket();
		}
		if (token.isJsapiTicketExpired()) {
			synchronized (globalJsapiTicketRefreshLock) {
				if (token.isJsapiTicketExpired()) {
					String url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket";
					String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
					JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
					JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject();
					String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
					int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
					token.updateJsapiTicket(jsapiTicket, expiresInSeconds);
				}
			}
		}
		return token.getJsapiTicket();
	}

	/**
	 * <pre>
	 * 获得jsapi_ticket
	 * 获得时会检查jsapiToken是否过期，如果过期了，那么就刷新一下，否则就什么都不干
	 * 
	 * 详情请见：http://qydev.weixin.qq.com/wiki/index.php?title=微信JS接口#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
	 * </pre>
	 * 
	 * @param forceRefresh
	 *            强制刷新
	 * @return
	 * @throws WxErrorException
	 */
	public String getJsapiTicket() throws WxErrorException {
		return getJsapiTicket(false);
	}

	/**
	 * <pre>
	 * 创建调用jsapi时所需要的签名
	 * 
	 * 详情请见：http://qydev.weixin.qq.com/wiki/index.php?title=微信JS接口#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
	 * </pre>
	 * 
	 * @param url
	 *            url
	 * @return
	 */
	public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
		long timestamp = System.currentTimeMillis() / 1000;
		String noncestr = RandomUtils.getRandomStr(16);
		String jsapiTicket = getJsapiTicket(false);
		String signature = SHA1.genWithAmple("jsapi_ticket=" + jsapiTicket, "noncestr=" + noncestr,
				"timestamp=" + timestamp, "url=" + url);
		WxJsapiSignature jsapiSignature = new WxJsapiSignature();
		jsapiSignature.setTimestamp(timestamp);
		jsapiSignature.setNoncestr(noncestr);
		jsapiSignature.setUrl(url);
		jsapiSignature.setSignature(signature);
		return jsapiSignature;
	}

	/**
	 * <pre>
	 * 上传多媒体文件
	 * 上传的多媒体文件有格式和大小限制，如下：
	 *   图片（image）: 1M，支持JPG格式
	 *   语音（voice）：2M，播放长度不超过60s，支持AMR\MP3格式
	 *   视频（video）：10MB，支持MP4格式
	 *   缩略图（thumb）：64KB，支持JPG格式
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
	 * </pre>
	 *
	 * @param mediaType
	 *            媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts}
	 * @param fileType
	 *            文件类型，请看{@link me.chanjar.weixin.common.api.WxConsts}
	 * @param inputStream
	 *            输入流
	 * @throws WxErrorException
	 */
	public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream)
			throws WxErrorException, IOException {
		return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
	}

	/**
	 * @param mediaType
	 * @param file
	 * @throws WxErrorException
	 * @see #mediaUpload(String, String, InputStream)
	 */
	public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
		return execute(new MediaUploadRequestExecutor(), url, file, null);
	}

	/**
	 * <pre>
	 * 下载多媒体文件
	 * 根据微信文档，视频文件下载不了，会返回null
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
	 * </pre>
	 *
	 * @return 保存到本地的临时文件
	 * @throws WxErrorException
	 * @params media_id
	 */
	public File mediaDownload(String media_id, String code) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get";
		return execute(new MediaDownloadRequestExecutor(queryAccessToken().getTmpDirFile()), url,
				"media_id=" + media_id);
	}

	/**
	 * <pre>
	 * 发送消息
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=发送消息
	 * </pre>
	 *
	 * @param message
	 * @throws WxErrorException
	 */
	public String messageSend(WxCpMessage message) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send";
		message.setAgentid(agentId);
		if (StringUtils.isNoneBlank(message.getUrl())) {
			String visitUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + corpId + "&redirect_uri="
					+ message.getUrl() + "&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect";
			message.setUrl(visitUrl);
			//cjianquan 2019/4/16 跳转的url是card中的url,而不是message的url
			if(message.getTextcard()!=null){
				message.getTextcard().setUrl(message.getUrl());
			}
		}
		System.out.println("message data:"+ JSON.toJSONString(message, true));
		return post(url, JSON.toJSONString(message, true));
	}

	/**
	 * <pre>
	 * 自定义菜单创建接口
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
	 * 
	 * 注意: 这个方法使用WxCpConfigStorage里的agentId
	 * </pre>
	 * 
	 * @see #menuCreate(String, me.chanjar.weixin.common.bean.WxMenu)
	 *
	 * @param menu
	 * @throws WxErrorException
	 */
	public void menuCreate(WxMenu menu, String code) throws WxErrorException {
		WxCpAccessToken token = queryAccessToken(code);
		menuCreate(token.getAgentId(), menu);
	}

	/**
	 * <pre>
	 * 自定义菜单创建接口
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
	 * 
	 * 注意: 这个方法不使用WxCpConfigStorage里的agentId，需要开发人员自己给出
	 * </pre>
	 * 
	 * @see #menuCreate(me.chanjar.weixin.common.bean.WxMenu)
	 *
	 * @param agentId
	 *            企业号应用的id
	 * @param menu
	 * @throws WxErrorException
	 */
	public void menuCreate(String agentId, WxMenu menu) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid=" + agentId;
		post(url, menu.toJson());
	}

	/**
	 * <pre>
	 * 自定义菜单删除接口
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
	 * 
	 * 注意: 这个方法使用WxCpConfigStorage里的agentId
	 * </pre>
	 * 
	 * @see #menuDelete(String)
	 *
	 * @throws WxErrorException
	 */
	public void menuDelete(String code) throws WxErrorException {
		menuDelete(queryAccessToken(code).getAgentId());
	}

	/**
	 * <pre>
	 * 自定义菜单删除接口
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
	 * 
	 * 注意: 这个方法不使用WxCpConfigStorage里的agentId，需要开发人员自己给出
	 * </pre>
	 * 
	 * @see #menuDelete()
	 *
	 * @param agentId
	 *            企业号应用的id
	 * @throws WxErrorException
	 */
	public void menuDelete(String agentId, String code) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?agentid=" + agentId;
		get(url, null);
	}

	/**
	 * <pre>
	 * 自定义菜单查询接口
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
	 * 
	 * 注意: 这个方法使用WxCpConfigStorage里的agentId
	 * </pre>
	 * 
	 * @see #menuGet(String)
	 *
	 * @return
	 * @throws WxErrorException
	 */
	public WxMenu menuGet() throws WxErrorException {
		return menuGet(queryAccessToken().getAgentId());
	}

	/**
	 * <pre>
	 * 自定义菜单查询接口
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
	 * 
	 * 注意: 这个方法不使用WxCpConfigStorage里的agentId，需要开发人员自己给出
	 * </pre>
	 * 
	 * @see #menuGet()
	 *
	 * @param agentId
	 *            企业号应用的id
	 * @return
	 * @throws WxErrorException
	 */
	public WxMenu menuGet(String agentId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?agentid=" + agentId;
		try {
			String resultContent = get(url, null);
			return WxMenu.fromJson(resultContent);
		} catch (WxErrorException e) {
			// 46003 不存在的菜单数据
			if (e.getError().getErrorCode() == 46003) {
				return null;
			}
			throw e;
		}
	}

	/**
	 * <pre>
	 * 部门管理接口 - 创建部门
	 * 最多支持创建500个部门
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=部门管理接口
	 * </pre>
	 *
	 * @param depart
	 *            部门
	 * @return 部门id
	 * @throws WxErrorException
	 */
	public Integer departCreate(WxCpDepart depart) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/department/create";
		String responseContent = execute(new SimplePostRequestExecutor(), url, depart.toJson(), "contacts");
		System.out.println(responseContent);
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return GsonHelper.getAsInteger(tmpJsonElement.getAsJsonObject().get("id"));
	}

	/**
	 * <pre>
	 * 部门管理接口 - 查询所有部门
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=部门管理接口
	 * </pre>
	 *
	 * @return
	 * @throws WxErrorException
	 */
	public List<WxCpDepart> departGet(String id) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/department/list";
		String params = "";
		if (StringUtils.isNoneBlank(id)) {
			params += "&id=" + id;
		}
		String responseContent = get(url, params, "contacts");
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxCpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("department"),
				new TypeToken<List<WxCpDepart>>() {}.getType());
	}

	/**
	 * <pre>
	 * 部门管理接口 - 修改部门名
	 * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=部门管理接口
	 * 如果id为0(未部门),1(黑名单),2(星标组)，或者不存在的id，微信会返回系统繁忙的错误
	 * </pre>
	 *
	 * @param group
	 *            要更新的group，group的id,name必须设置
	 * @throws WxErrorException
	 */
	public void departUpdate(WxCpDepart group) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/department/update";
		post(url, group.toJson(), "contacts");
	}

	/**
	 * <pre>
	 * 部门管理接口 - 删除部门
	 * </pre>
	 *
	 * @param departId
	 * @throws WxErrorException
	 */
	public void departDelete(Integer departId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/department/delete?id=" + departId;
		get(url, null, "contacts");
	}

	/**
	 * <pre>
	 * 获取部门成员(详情)
	 * 
	 * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98.28.E8.AF.A6.E6.83.85.29
	 * </pre>
	 * 
	 * @param departId
	 *            必填。部门id
	 * @param fetchChild
	 *            非必填。1/0：是否递归获取子部门下面的成员
	 * @param status
	 *            非必填。0获取全部员工，1获取已关注成员列表，2获取禁用成员列表，4获取未关注成员列表。status可叠加
	 * @return
	 * @throws WxErrorException
	 */
	public List<WxCpUser> userList(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/list?department_id=" + departId;
		String params = "";
		if (fetchChild != null) {
			params += "&fetch_child=" + (fetchChild ? "1" : "0");
		}
		if (status != null) {
			params += "&status=" + status;
		} else {
			params += "&status=0";
		}

		String responseContent = get(url, params, "contacts");
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxCpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("userlist"),
				new TypeToken<List<WxCpUser>>() {
				}.getType());
	}

	/**
	 * <pre>
	 * 获取部门成员
	 * 
	 * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98
	 * </pre>
	 *
	 * @param departId
	 *            必填。部门id
	 * @param fetchChild
	 *            非必填。1/0：是否递归获取子部门下面的成员
	 * @param status
	 *            非必填。0获取全部员工，1获取已关注成员列表，2获取禁用成员列表，4获取未关注成员列表。status可叠加
	 * @return
	 * @throws WxErrorException
	 */
	public List<WxCpUser> departGetUsers(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?department_id=" + departId;
		String params = "";
		if (fetchChild != null) {
			params += "&fetch_child=" + (fetchChild ? "1" : "0");
		}
		if (status != null) {
			params += "&status=" + status;
		} else {
			params += "&status=0";
		}

		String responseContent = get(url, params, "contacts");
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxCpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("userlist"),
				new TypeToken<List<WxCpUser>>() {
				}.getType());
	}

	/**
	 * 新建用户
	 *
	 * @param user
	 * @throws WxErrorException
	 */
	public void userCreate(WxCpUser user) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/create";
		post(url, user.toJson(), "contacts");
	}

	/**
	 * 更新用户
	 *
	 * @param user
	 * @throws WxErrorException
	 */
	public void userUpdate(WxCpUser user) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/update";
		post(url, user.toJson(), "contacts");
	}

	/**
	 * 删除用户
	 *
	 * @param userid
	 * @throws WxErrorException
	 */
	public void userDelete(String userid) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/delete?userid=" + userid;
		get(url, null, "contacts");
	}

	/**
	 * <pre>
	 * 批量删除成员
	 * 
	 * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E6.89.B9.E9.87.8F.E5.88.A0.E9.99.A4.E6.88.90.E5.91.98
	 * </pre>
	 * 
	 * @param userids
	 *            员工UserID列表。对应管理端的帐号
	 * @throws WxErrorException
	 */
	public void userDelete(String[] userids) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete";
		JsonObject jsonObject = new JsonObject();
		JsonArray jsonArray = new JsonArray();
		for (int i = 0; i < userids.length; i++) {
			jsonArray.add(new JsonPrimitive(userids[i]));
		}
		jsonObject.add("useridlist", jsonArray);
		post(url, jsonObject.toString(), "contacts");
	}

	/**
	 * 获取用户
	 *
	 * @param userid
	 * @return
	 * @throws WxErrorException
	 */
	public WxCpUser userGet(String userid) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/get?userid=" + userid;
		String responseContent = get(url, null, "contacts");
		return WxCpUser.fromJson(responseContent);
	}

	/**
	 * userid与openid互换
	 * 
	 * @param userid
	 * @return
	 * @throws WxErrorException
	 */
	public String convertToOpenid(String userid) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid";
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("userid", userid);
		String responseContent = post(url, jsonObject.toString(), "contacts");

		JsonElement je = Streams.parse(new JsonReader(new StringReader(responseContent)));
		JsonObject jo = je.getAsJsonObject();
		return GsonHelper.getString(jo, "openid");
	}

	/**
	 * 创建标签
	 *
	 * @param tagName
	 * @return
	 */
	public String tagCreate(String tagName) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/create";
		JsonObject o = new JsonObject();
		o.addProperty("tagname", tagName);
		String responseContent = post(url, o.toString(), "contacts");
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return tmpJsonElement.getAsJsonObject().get("tagid").getAsString();
	}

	/**
	 * 更新标签
	 *
	 * @param tagId
	 * @param tagName
	 */
	public void tagUpdate(String tagId, String tagName) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/update";
		JsonObject o = new JsonObject();
		o.addProperty("tagid", tagId);
		o.addProperty("tagname", tagName);
		post(url, o.toString(), "contacts");
	}

	/**
	 * 删除标签
	 *
	 * @param tagId
	 */
	public void tagDelete(String tagId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?tagid=" + tagId;
		get(url, null, "contacts");
	}

	/**
	 * 获得标签列表
	 *
	 * @return
	 */
	public List<WxCpTag> tagGet() throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/list";
		String responseContent = get(url, null, "contacts");
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxCpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("taglist"),
				new TypeToken<List<WxCpTag>>() {
				}.getType());
	}

	/**
	 * 获取标签成员
	 *
	 * @param tagId
	 * @return
	 */
	public List<WxCpUser> tagGetUsers(String tagId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?tagid=" + tagId;
		String responseContent = get(url, null, "contacts");
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxCpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("userlist"),
				new TypeToken<List<WxCpUser>>() {
				}.getType());
	}

	/**
	 * 增加标签成员
	 *
	 * @param tagId
	 * @param userIds
	 */
	public void tagAddUsers(String tagId, List<String> userIds, List<String> partyIds) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers";
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("tagid", tagId);
		if (userIds != null) {
			JsonArray jsonArray = new JsonArray();
			for (String userId : userIds) {
				jsonArray.add(new JsonPrimitive(userId));
			}
			jsonObject.add("userlist", jsonArray);
		}
		if (partyIds != null) {
			JsonArray jsonArray = new JsonArray();
			for (String userId : partyIds) {
				jsonArray.add(new JsonPrimitive(userId));
			}
			jsonObject.add("partylist", jsonArray);
		}
		post(url, jsonObject.toString(), "contacts");
	}

	/**
	 * <pre>
	 * 构造oauth2授权的url连接
	 * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
	 * </pre>
	 * 
	 * @param redirectUri
	 * @param state
	 * @return url
	 */
	public String oauth2buildAuthorizationUrl(String redirectUri, String state) throws WxErrorException {
		String url = "https://open.weixin.qq.com/connect/oauth2/authorize?";
		url += "appid=" + queryAccessToken().getCorpId();
		url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectUri);
		url += "&response_type=code";
		url += "&scope=snsapi_base";
		if (state != null) {
			url += "&state=" + state;
		}
		url += "#wechat_redirect";
		return url;
	}

	/**
	 * <pre>
	 * 用oauth2获取用户信息
	 * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
	 * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名，所以无法测试，因此这个方法很可能是坏的。
	 * 
	 * 注意: 这个方法使用WxCpConfigStorage里的agentId
	 * </pre>
	 * 
	 * @see #oauth2getUserInfo(String, String)
	 *
	 * @param code
	 * @return [userid, deviceid]
	 */
	public String oauth2getUserInfo(String code) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?" + "code=" + code;
		String responseText = get(url, null, "yywork");
		if (StringUtils.isBlank(responseText)) {
			return "";
		}
		JsonElement je = Streams.parse(new JsonReader(new StringReader(responseText)));
		JsonObject jo = je.getAsJsonObject();
		return GsonHelper.getString(jo, "UserId");
	}

	/**
	 * <pre>
	 * 用oauth2获取用户信息
	 * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
	 * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名，所以无法测试，因此这个方法很可能是坏的。
	 * 
	 * 注意: 这个方法不使用WxCpConfigStorage里的agentId，需要开发人员自己给出
	 * </pre>
	 * 
	 * @see #oauth2getUserInfo(String)
	 *
	 * @param agentId
	 *            企业号应用的id
	 * @param code
	 * @return [userid, deviceid]
	 */
	public String[] oauth2getUserInfo(String agentId, String code) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?" + "code=" + code + "&agendid=" + agentId;
		String responseText = get(url, null);
		JsonElement je = Streams.parse(new JsonReader(new StringReader(responseText)));
		JsonObject jo = je.getAsJsonObject();
		return new String[] { GsonHelper.getString(jo, "UserId"), GsonHelper.getString(jo, "DeviceId") };
	}

	/**
	 * 移除标签成员
	 *
	 * @param tagId
	 * @param userIds
	 */
	public void tagRemoveUsers(String tagId, List<String> userIds) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers";
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("tagid", tagId);
		JsonArray jsonArray = new JsonArray();
		for (String userId : userIds) {
			jsonArray.add(new JsonPrimitive(userId));
		}
		jsonObject.add("userlist", jsonArray);
		post(url, jsonObject.toString(), "contacts");
	}

	/**
	 * <pre>
	 * 邀请成员关注
	 * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8
	 * </pre>
	 * 
	 * @param userId
	 *            用户的userid
	 * @param inviteTips
	 *            推送到微信上的提示语（只有认证号可以使用）。当使用微信推送时，该字段默认为“请关注XXX企业号”，邮件邀请时，该字段无效。
	 * @return 1:微信邀请 2.邮件邀请
	 * @throws WxErrorException
	 */
	public int invite(String userId, String inviteTips) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/invite/send";
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("userid", userId);
		if (StringUtils.isNotEmpty(inviteTips)) {
			jsonObject.addProperty("invite_tips", inviteTips);
		}
		String responseContent = post(url, jsonObject.toString());
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return tmpJsonElement.getAsJsonObject().get("type").getAsInt();
	}

	/**
	 * 上传部门列表覆盖企业号上的部门信息
	 * 
	 * @param mediaId
	 * @throws WxErrorException
	 */
	public String replaceParty(String mediaId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceparty";
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("media_id", mediaId);
		return post(url, jsonObject.toString());
	}

	/**
	 * 上传用户列表覆盖企业号上的用户信息
	 * 
	 * @param mediaId
	 * @throws WxErrorException
	 */
	public String replaceUser(String mediaId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceuser";
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("media_id", mediaId);
		return post(url, jsonObject.toString());
	}

	/**
	 * 获取异步任务结果
	 * 
	 * @param joinId
	 * @return
	 * @throws WxErrorException
	 */
	public String getTaskResult(String joinId) throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/getresult?jobid=" + joinId;
		return get(url, null);
	}

	/**
	 * <pre>
	 * 获取微信服务器的ip段
	 * http://qydev.weixin.qq.com/wiki/index.php?title=回调模式#.E8.8E.B7.E5.8F.96.E5.BE.AE.E4.BF.A1.E6.9C.8D.E5.8A.A1.E5.99.A8.E7.9A.84ip.E6.AE.B5
	 * </pre>
	 * 
	 * @return { "ip_list": ["101.226.103.*", "101.226.62.*"] }
	 * @throws WxErrorException
	 */
	public String[] getCallbackIp() throws WxErrorException {
		String url = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip";
		String responseContent = get(url, null);
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		JsonArray jsonArray = tmpJsonElement.getAsJsonObject().get("ip_list").getAsJsonArray();
		String[] ips = new String[jsonArray.size()];
		for (int i = 0; i < jsonArray.size(); i++) {
			ips[i] = jsonArray.get(i).getAsString();
		}
		return ips;
	}

	/**
	 * 当本Service没有实现某个API的时候，可以用这个，针对所有微信API中的GET请求
	 * 
	 * @param url
	 * @param queryParam
	 * @param code
	 * @return
	 * @throws WxErrorException
	 */
	public String get(String url, String queryParam, String code) throws WxErrorException {
		return execute(new SimpleGetRequestExecutor(), url, queryParam, code);
	}

	/**
	 * 当本Service没有实现某个API的时候，可以用这个，针对所有微信API中的GET请求
	 * 
	 * @param url
	 * @param queryParam
	 * @return
	 * @throws WxErrorException
	 */
	public String get(String url, String queryParam) throws WxErrorException {
		return execute(new SimpleGetRequestExecutor(), url, queryParam, null);
	}

	/**
	 * 当本Service没有实现某个API的时候，可以用这个，针对所有微信API中的POST请求
	 * 
	 * @param url
	 * @param postData
	 * @param code
	 * @return
	 * @throws WxErrorException
	 */
	public String post(String url, String postData, String code) throws WxErrorException {
		return execute(new SimplePostRequestExecutor(), url, postData, code);
	}

	/**
	 * 当本Service没有实现某个API的时候，可以用这个，针对所有微信API中的POST请求
	 * 
	 * @param url
	 * @param postData
	 * @return
	 * @throws WxErrorException
	 */
	public String post(String url, String postData) throws WxErrorException {
		return execute(new SimplePostRequestExecutor(), url, postData, null);
	}

	/**
	 * <pre>
	 * Service没有实现某个API的时候，可以用这个，
	 * 比{@link #get}和{@link #post}方法更灵活，可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
	 * 可以参考，{@link me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor}的实现方法
	 * </pre>
	 * 
	 * @param executor
	 * @param uri
	 * @param data
	 * @param <T>
	 * @param <E>
	 * @return
	 * @throws WxErrorException
	 */
	public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data, String code) throws WxErrorException {
		int retryTimes = 0;
		do {
			try {
				return executeInternal(executor, uri, data, code);
			} catch (WxErrorException e) {
				WxError error = e.getError();
				/**
				 * -1 系统繁忙, 1000ms后重试
				 */
				if (error.getErrorCode() == -1) {
					int sleepMillis = retrySleepMillis * (1 << retryTimes);
					try {
						log.debug("微信系统繁忙，{}ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
						Thread.sleep(sleepMillis);
					} catch (InterruptedException e1) {
						throw new RuntimeException(e1);
					}
				} else {
					throw e;
				}
			}
		} while (++retryTimes < maxRetryTimes);

		throw new RuntimeException("微信服务端异常，超出重试次数");
	}

	public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
		return this.execute(executor, uri, data, null);
	}

	protected synchronized <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data, String code)
			throws WxErrorException {
		if (uri.indexOf("access_token=") != -1) {
			throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
		}
		String accessToken = getAccessToken(false, code);

		String uriWithAccessToken = uri;
		uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;

		try {
			return executor.execute(getHttpclient(), uriWithAccessToken, data);
		} catch (WxErrorException e) {
			WxError error = e.getError();
			/*
			 * 发生以下情况时尝试刷新access_token 40001
			 * 获取access_token时AppSecret错误，或者access_token无效 42001 access_token超时
			 */
			if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) {
				resetAccessToken(code);
				return execute(executor, uri, data, code);
			}
			if (error.getErrorCode() != 0) {
				throw new WxErrorException(error);
			}
			return null;
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public String getAccessToken() throws WxErrorException {
		return this.getAccessToken(false, null);
	}

	public String getAccessToken(boolean forceRefresh, String code) throws WxErrorException {
		if (StringUtils.isBlank(code)) {
			code = "yywork";
		}
		WxCpAccessToken token = queryTokenByCode(code);
		if (token == null) {
			return "";
		}
		if (forceRefresh) {
			token.expireAccessToken();
		}
		if (token.isAccessTokenExpired()) {
			synchronized (globalAccessTokenRefreshLock) {
				if (token.isAccessTokenExpired()) {
					String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" + "&corpid=" + getCorpId()
							+ "&corpsecret=" + token.getCorpSecret();
					try {
						HttpGet httpGet = new HttpGet(url);
						CloseableHttpClient httpclient = getHttpclient();
						String resultContent = null;
						try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
							resultContent = new BasicResponseHandler().handleResponse(response);
						}
						WxError error = WxError.fromJson(resultContent);
						if (error.getErrorCode() != 0) {
							throw new WxErrorException(error);
						}
						WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
						token.setAccessToken(accessToken.getAccessToken());
						token.setExpiresTime(accessToken.getExpiresIn());
						updateByToken(token.getId(), accessToken.getAccessToken(), accessToken.getExpiresIn());
					} catch (ClientProtocolException e) {
						throw new RuntimeException(e);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}
			}
		}
		return token.getAccessToken();
	}

	public WxCpAccessToken queryAccessToken() throws WxErrorException {
		return this.queryAccessToken(null);
	}

	public WxCpAccessToken queryAccessToken(String code) throws WxErrorException {
		if (StringUtils.isBlank(code)) {
			code = "yoooyao";
		}
		WxCpAccessToken token = queryTokenByCode(code);
		if (token == null) {
			return null;
		}
		if (token.isAccessTokenExpired()) {
			synchronized (globalAccessTokenRefreshLock) {
				if (token.isAccessTokenExpired()) {
					String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" + "&corpid=" + getCorpId()
							+ "&corpsecret=" + token.getCorpSecret();
					try {
						HttpGet httpGet = new HttpGet(url);
						CloseableHttpClient httpclient = getHttpclient();
						String resultContent = null;
						try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
							resultContent = new BasicResponseHandler().handleResponse(response);
						}
						WxError error = WxError.fromJson(resultContent);
						if (error.getErrorCode() != 0) {
							throw new WxErrorException(error);
						}
						WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
						updateByToken(token.getId(), accessToken.getAccessToken(), accessToken.getExpiresIn());
					} catch (ClientProtocolException e) {
						throw new RuntimeException(e);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}
			}
		}
		return token;
	}

	public void resetAccessToken(String code) throws WxErrorException {
		WxCpAccessToken token = queryTokenByCode(code);
		if (token != null) {
			synchronized (globalAccessTokenRefreshLock) {
				String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" + "&corpid=" + getCorpId() + "&corpsecret="
						+ token.getCorpSecret();
				try {
					HttpGet httpGet = new HttpGet(url);
					CloseableHttpClient httpclient = getHttpclient();
					String resultContent = null;
					try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
						resultContent = new BasicResponseHandler().handleResponse(response);
					}
					WxError error = WxError.fromJson(resultContent);
					if (error.getErrorCode() != 0) {
						throw new WxErrorException(error);
					}
					WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
					updateByToken(token.getId(), accessToken.getAccessToken(), accessToken.getExpiresIn());
				} catch (ClientProtocolException e) {
					throw new RuntimeException(e);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
		}

	}

	/**
	 * 根据编号查询AccessToken
	 * 
	 * @param code
	 * @return
	 */
	public WxCpAccessToken queryTokenByCode(String code) {
		AccessTokenPo token = this.accessTokenMapper.queryByCode(code);
		if (token != null) {
			WxCpAccessToken cpToken = new WxCpAccessToken();
			BeanUtils.copyProperties(token, cpToken);
			return cpToken;
		}
		return null;
	}

	/**
	 * 更新AccessToken
	 * 
	 * @param id
	 * @param accessToken
	 * @param expiresIn
	 */
	public void updateByToken(Long id, String accessToken, Integer expiresIn) {
		Date now = new Date();
		accessTokenMapper.updateToken(id, accessToken, expiresIn, now.getTime(), new Date());
	}

	protected CloseableHttpClient getHttpclient() {
		return HttpClientUtils.createSSLClientDefault();
	}

	public File getTmpDirFile() {
		return tmpDirFile;
	}

	public void setTmpDirFile(File tmpDirFile) {
		this.tmpDirFile = tmpDirFile;
	}

	public String getCorpId() {
		return corpId;
	}

}
